﻿#if (!PCL) && ((!UNITY_5) || UNITY_STANDALONE)

using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Debugging;
using MoonSharp.VsCodeDebugger.SDK;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;


namespace MoonSharp.VsCodeDebugger.DebuggerLogic
{
    internal class MoonSharpDebugSession : DebugSession, IAsyncDebuggerClient
    {
        AsyncDebugger m_Debug;
        MoonSharpVsCodeDebugServer m_Server;
        List<DynValue> m_Variables = new List<DynValue>();
        bool m_NotifyExecutionEnd = false;

        const int SCOPE_LOCALS = 65536;
        const int SCOPE_SELF = 65537;


        internal MoonSharpDebugSession(MoonSharpVsCodeDebugServer server, AsyncDebugger debugger)
            : base(true, false)
        {
            m_Server = server;
            m_Debug = debugger;
        }

        public override void Initialize(Response response, Table args)
        {
#if DOTNET_CORE
			SendText("Connected to MoonSharp {0} [{1}]",
					 Script.VERSION,
					 Script.GlobalOptions.Platform.GetPlatformName());
#else
            SendText("MoonSharp Debug Session - Connected to MoonSharp {0} [{1}] on process {2} (PID {3})",
                Script.VERSION,
                Script.GlobalOptions.Platform.GetPlatformName(),
                System.Diagnostics.Process.GetCurrentProcess().ProcessName,
                System.Diagnostics.Process.GetCurrentProcess().Id);
#endif

            SendText("Debugging script '{0}'; use the debug console to debug another script.", m_Debug.Name);

            SendText("Type '!help' in the Debug Console for available commands.");

            SendResponse(response, new Capabilities()
            {
                // This debug adapter does not need the configurationDoneRequest.
                supportsConfigurationDoneRequest = false,

                // This debug adapter does not support function breakpoints.
                supportsFunctionBreakpoints = false,

                // This debug adapter doesn't support conditional breakpoints.
                supportsConditionalBreakpoints = false,

                // This debug adapter does not support a side effect free evaluate request for data hovers.
                supportsEvaluateForHovers = false,

                // This debug adapter does not support exception breakpoint filters
                exceptionBreakpointFilters = new object[0]
            });

            // Debugger is ready to accept breakpoints immediately
            SendEvent(new InitializedEvent());

            m_Debug.Client = this;
        }

        public override void Attach(Response response, Table arguments)
        {
            SendResponse(response);
        }

        public override void Continue(Response response, Table arguments)
        {
            m_Debug.QueueAction(new DebuggerAction() {Action = DebuggerAction.ActionType.Run});
            SendResponse(response);
        }

        public override void Disconnect(Response response, Table arguments)
        {
            m_Debug.Client = null;
            SendResponse(response);
        }

        private static string getString(Table args, string property, string dflt = null)
        {
            var s = (string) args[property];
            if (s == null)
            {
                return dflt;
            }

            s = s.Trim();
            if (s.Length == 0)
            {
                return dflt;
            }

            return s;
        }

        public override void Evaluate(Response response, Table args)
        {
            var expression = getString(args, "expression");
            var frameId = getInt(args, "frameId", 0);
            var context = getString(args, "context") ?? "hover";

            if (frameId != 0 && context != "repl")
                SendText("Warning : Evaluation of variables/watches is always done with the top-level scope.");

            if (context == "repl" && expression.StartsWith("!"))
            {
                ExecuteRepl(expression.Substring(1));
                SendResponse(response);
                return;
            }

            DynValue v = m_Debug.Evaluate(expression) ?? DynValue.Nil;
            m_Variables.Add(v);

            SendResponse(response, new EvaluateResponseBody(v.ToDebugPrintString(), m_Variables.Count - 1)
            {
                type = v.Type.ToLuaDebuggerString()
            });
        }

        private void ExecuteRepl(string cmd)
        {
            bool showHelp = false;
            cmd = cmd.Trim();
            if (cmd == "help")
            {
                showHelp = true;
            }
            else if (cmd.StartsWith("geterror"))
            {
                SendText("Current error regex : {0}", m_Debug.ErrorRegex.ToString());
            }
            else if (cmd.StartsWith("seterror"))
            {
                string regex = cmd.Substring("seterror".Length).Trim();

                try
                {
                    Regex rx = new Regex(regex);
                    m_Debug.ErrorRegex = rx;
                    SendText("Current error regex : {0}", m_Debug.ErrorRegex.ToString());
                }
                catch (Exception ex)
                {
                    SendText("Error setting regex: {0}", ex.Message);
                }
            }
            else if (cmd.StartsWith("execendnotify"))
            {
                string val = cmd.Substring("execendnotify".Length).Trim();

                if (val == "off")
                {
                    m_NotifyExecutionEnd = false;
                }
                else if (val == "on")
                {
                    m_NotifyExecutionEnd = true;
                }
                else if (val.Length > 0)
                    SendText("Error : expected 'on' or 'off'");

                SendText("Notifications of execution end are : {0}", m_NotifyExecutionEnd ? "enabled" : "disabled");
            }
            else if (cmd == "list")
            {
                int currId = m_Server.CurrentId ?? -1000;

                foreach (var pair in m_Server.GetAttachedDebuggersByIdAndName())
                {
                    string isthis = (pair.Key == m_Debug.Id) ? " (this)" : "";
                    string isdef = (pair.Key == currId) ? " (default)" : "";

                    SendText("{0} : {1}{2}{3}", pair.Key.ToString().PadLeft(9), pair.Value, isdef, isthis);
                }
            }
            else if (cmd.StartsWith("select") || cmd.StartsWith("switch"))
            {
                string arg = cmd.Substring("switch".Length).Trim();

                try
                {
                    int id = int.Parse(arg);
                    m_Server.CurrentId = id;

                    if (cmd.StartsWith("switch"))
                        Unbind();
                    else
                        SendText("Next time you'll attach the debugger, it will be atteched to script #{0}", id);
                }
                catch (Exception ex)
                {
                    SendText("Error setting regex: {0}", ex.Message);
                }
            }
            else
            {
                SendText("Syntax error : {0}\n", cmd);
                showHelp = true;
            }

            if (showHelp)
            {
                SendText("Available commands : ");
                SendText("    !help - gets this help");
                SendText("    !list - lists the other scripts which can be debugged");
                SendText("    !select <id> - select another script for future sessions");
                SendText("    !switch <id> - switch to another script (same as select + disconnect)");
                SendText("    !seterror <regex> - sets the regex which tells which errors to trap");
                SendText("    !geterror - gets the current value of the regex which tells which errors to trap");
                SendText(
                    "    !execendnotify [on|off] - sets the notification of end of execution on or off (default = off)");
                SendText("    ... or type an expression to evaluate it on the fly.");
            }
        }


        public override void Launch(Response response, Table arguments)
        {
            SendResponse(response);
        }

        public override void Next(Response response, Table arguments)
        {
            m_Debug.QueueAction(new DebuggerAction() {Action = DebuggerAction.ActionType.StepOver});
            SendResponse(response);
        }

        private StoppedEvent CreateStoppedEvent(string reason, string text = null)
        {
            return new StoppedEvent(0, reason, text);
        }

        public override void Pause(Response response, Table arguments)
        {
            m_Debug.PauseRequested = true;
            SendResponse(response);
            SendText("Pause pending -- will pause at first script statement.");
        }

        public override void Scopes(Response response, Table arguments)
        {
            var scopes = new List<Scope>();

            scopes.Add(new Scope("Locals", SCOPE_LOCALS));
            scopes.Add(new Scope("Self", SCOPE_SELF));

            SendResponse(response, new ScopesResponseBody(scopes));
        }

        public override void SetBreakpoints(Response response, Table args)
        {
            string path = null;

            Table args_source = args["source"] as Table;

            if (args_source != null)
            {
                string p = args_source["path"].ToString();
                if (p != null && p.Trim().Length > 0)
                    path = p;
            }

            if (path == null)
            {
                SendErrorResponse(response, 3010, "setBreakpoints: property 'source' is empty or misformed", null,
                    false, true);
                return;
            }

            path = ConvertClientPathToDebugger(path);

            SourceCode src = m_Debug.FindSourceByName(path);

            if (src == null)
            {
                // we only support breakpoints in files mono can handle
                SendResponse(response, new SetBreakpointsResponseBody());
                return;
            }

            Table clientLines = args.Get("lines").Table;

            var lin = new HashSet<int>(clientLines.Values.Select(jt => ConvertClientLineToDebugger(jt.ToObject<int>()))
                .ToArray());

            var lin2 = m_Debug.DebugService.ResetBreakPoints(src, lin);

            var breakpoints = new List<Breakpoint>();
            foreach (var l in lin)
            {
                breakpoints.Add(new Breakpoint(lin2.Contains(l), l));
            }

            response.SetBody(new SetBreakpointsResponseBody(breakpoints));
            SendResponse(response);
        }

        public override void StackTrace(Response response, Table args)
        {
            int maxLevels = getInt(args, "levels", 10);
            //int threadReference = getInt(args, "threadId", 0);

            var stackFrames = new List<StackFrame>();

            var stack = m_Debug.GetWatches(WatchType.CallStack);

            var coroutine = m_Debug.GetWatches(WatchType.Threads).LastOrDefault();

            int level = 0;
            int max = Math.Min(maxLevels - 3, stack.Count);

            while (level < max)
            {
                WatchItem frame = stack[level];

                string name = frame.Name;
                SourceRef sourceRef = frame.Location ?? DefaultSourceRef;
                int sourceIdx = sourceRef.SourceIdx;
                string path = sourceRef.IsClrLocation ? "(native)" : (m_Debug.GetSourceFile(sourceIdx) ?? "???");
                string sourceName = Path.GetFileName(path);

                var source = new Source(sourceName, path); // ConvertDebuggerPathToClient(path));

                stackFrames.Add(new StackFrame(level, name, source,
                    ConvertDebuggerLineToClient(sourceRef.FromLine), sourceRef.FromChar,
                    ConvertDebuggerLineToClient(sourceRef.ToLine), sourceRef.ToChar));

                level++;
            }

            if (stack.Count > maxLevels - 3)
                stackFrames.Add(new StackFrame(level++, "(...)", null, 0));

            if (coroutine != null)
                stackFrames.Add(new StackFrame(level++, "(" + coroutine.Name + ")", null, 0));
            else
                stackFrames.Add(new StackFrame(level++, "(main coroutine)", null, 0));

            stackFrames.Add(new StackFrame(level++, "(native)", null, 0));

            SendResponse(response, new StackTraceResponseBody(stackFrames));
        }

        readonly SourceRef DefaultSourceRef = new SourceRef(-1, 0, 0, 0, 0, false);


        private int getInt(Table args, string propName, int defaultValue)
        {
            var jo = args.Get(propName);

            if (jo.Type != DataType.Number)
                return defaultValue;
            else
                return jo.ToObject<int>();
        }


        public override void StepIn(Response response, Table arguments)
        {
            m_Debug.QueueAction(new DebuggerAction() {Action = DebuggerAction.ActionType.StepIn});
            SendResponse(response);
        }

        public override void StepOut(Response response, Table arguments)
        {
            m_Debug.QueueAction(new DebuggerAction() {Action = DebuggerAction.ActionType.StepOut});
            SendResponse(response);
        }

        public override void Threads(Response response, Table arguments)
        {
            var threads = new List<Thread>() {new Thread(0, "Main Thread")};
            SendResponse(response, new ThreadsResponseBody(threads));
        }


        public override void Variables(Response response, Table arguments)
        {
            int index = getInt(arguments, "variablesReference", -1);

            var variables = new List<Variable>();

            if (index == SCOPE_SELF)
            {
                DynValue v = m_Debug.Evaluate("self");
                VariableInspector.InspectVariable(v, variables);
            }
            else if (index == SCOPE_LOCALS)
            {
                foreach (var w in m_Debug.GetWatches(WatchType.Locals))
                    variables.Add(new Variable(w.Name, (w.Value ?? DynValue.Void).ToDebugPrintString()));
            }
            else if (index < 0 || index >= m_Variables.Count)
            {
                variables.Add(new Variable("<error>", null));
            }
            else
            {
                VariableInspector.InspectVariable(m_Variables[index], variables);
            }

            SendResponse(response, new VariablesResponseBody(variables));
        }

        void IAsyncDebuggerClient.SendStopEvent()
        {
            SendEvent(CreateStoppedEvent("step"));
        }


        void IAsyncDebuggerClient.OnWatchesUpdated(WatchType watchType)
        {
            if (watchType == WatchType.CallStack)
                m_Variables.Clear();
        }

        void IAsyncDebuggerClient.OnSourceCodeChanged(int sourceID)
        {
            if (m_Debug.IsSourceOverride(sourceID))
                SendText("Loaded source '{0}' -> '{1}'", m_Debug.GetSource(sourceID).Name,
                    m_Debug.GetSourceFile(sourceID));
            else
                SendText("Loaded source '{0}'", m_Debug.GetSource(sourceID).Name);
        }

        public void OnExecutionEnded()
        {
            if (m_NotifyExecutionEnd)
                SendText("Execution ended.");
        }

        public void SendText(string msg, params object[] args)
        {
            msg = string.Format(msg, args);
            // SendEvent(new OutputEvent("console", DateTime.Now.ToString("u") + ": " + msg + "\n"));
            SendEvent(new OutputEvent("console", msg + "\n"));
        }

        public void OnException(ScriptRuntimeException ex)
        {
            SendText("runtime error : {0}", ex.DecoratedMessage);
        }

        public void Unbind()
        {
            SendText("Debug session has been closed by the hosting process.");
            SendText("Bye.");
            SendEvent(new TerminatedEvent());
        }
    }
}
#endif